{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:20:20.064769Z",
     "start_time": "2021-01-12T16:20:19.787429Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Populating the interactive namespace from numpy and matplotlib\n"
     ]
    }
   ],
   "source": [
    "%pylab inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Notebook magic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-02-15T15:55:00.261542Z",
     "start_time": "2021-02-15T15:55:00.255639Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
    "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
    "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
    "import subprocess\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-02-15T15:55:02.228409Z",
     "start_time": "2021-02-15T15:55:02.207636Z"
    }
   },
   "outputs": [],
   "source": [
    "@magics_class\n",
    "class PyboardMagic(Magics):\n",
    "    @cell_magic\n",
    "    @magic_arguments()\n",
    "    @argument('-skip')\n",
    "    @argument('-unix')\n",
    "    @argument('-pyboard')\n",
    "    @argument('-file')\n",
    "    @argument('-data')\n",
    "    @argument('-time')\n",
    "    @argument('-memory')\n",
    "    def micropython(self, line='', cell=None):\n",
    "        args = parse_argstring(self.micropython, line)\n",
    "        if args.skip: # doesn't care about the cell's content\n",
    "            print('skipped execution')\n",
    "            return None # do not parse the rest\n",
    "        if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
    "            with open('/dev/shm/micropython.py', 'w') as fout:\n",
    "                fout.write(cell)\n",
    "            proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
    "                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
    "            print(proc.stdout.read().decode(\"utf-8\"))\n",
    "            print(proc.stderr.read().decode(\"utf-8\"))\n",
    "            return None\n",
    "        if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
    "            spaces = \"    \"\n",
    "            try:\n",
    "                with open(args.file, 'w') as fout:\n",
    "                    fout.write(cell.replace('\\t', spaces))\n",
    "                    printf('written cell to {}'.format(args.file))\n",
    "            except:\n",
    "                print('Failed to write to disc!')\n",
    "            return None # do not parse the rest\n",
    "        if args.data: # can be used to load data from the pyboard directly into kernel space\n",
    "            message = pyb.exec(cell)\n",
    "            if len(message) == 0:\n",
    "                print('pyboard >>>')\n",
    "            else:\n",
    "                print(message.decode('utf-8'))\n",
    "                # register new variable in user namespace\n",
    "                self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
    "        \n",
    "        if args.time: # measures the time of executions\n",
    "            pyb.exec('import utime')\n",
    "            message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
    "                               \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
    "            print(message.decode('utf-8'))\n",
    "        \n",
    "        if args.memory: # prints out memory information \n",
    "            message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
    "            print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "            message = pyb.exec(cell)\n",
    "            print(\">>> \", message.decode('utf-8'))\n",
    "            message = pyb.exec('print(mem_info())')\n",
    "            print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "\n",
    "        if args.pyboard:\n",
    "            message = pyb.exec(cell)\n",
    "            print(message.decode('utf-8'))\n",
    "\n",
    "ip = get_ipython()\n",
    "ip.register_magics(PyboardMagic)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pyboard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:35.126401Z",
     "start_time": "2020-05-07T07:35:35.105824Z"
    }
   },
   "outputs": [],
   "source": [
    "import pyboard\n",
    "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
    "pyb.enter_raw_repl()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-19T19:11:18.145548Z",
     "start_time": "2020-05-19T19:11:18.137468Z"
    }
   },
   "outputs": [],
   "source": [
    "pyb.exit_raw_repl()\n",
    "pyb.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:38.725924Z",
     "start_time": "2020-05-07T07:35:38.645488Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "import utime\n",
    "import ulab as np\n",
    "\n",
    "def timeit(n=1000):\n",
    "    def wrapper(f, *args, **kwargs):\n",
    "        func_name = str(f).split(' ')[1]\n",
    "        def new_func(*args, **kwargs):\n",
    "            run_times = np.zeros(n, dtype=np.uint16)\n",
    "            for i in range(n):\n",
    "                t = utime.ticks_us()\n",
    "                result = f(*args, **kwargs)\n",
    "                run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
    "            print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
    "            print('\\tbest: %d us'%np.min(run_times))\n",
    "            print('\\tworst: %d us'%np.max(run_times))\n",
    "            print('\\taverage: %d us'%np.mean(run_times))\n",
    "            print('\\tdeviation: +/-%.3f us'%np.std(run_times))            \n",
    "            return result\n",
    "        return new_func\n",
    "    return wrapper\n",
    "\n",
    "def timeit(f, *args, **kwargs):\n",
    "    func_name = str(f).split(' ')[1]\n",
    "    def new_func(*args, **kwargs):\n",
    "        t = utime.ticks_us()\n",
    "        result = f(*args, **kwargs)\n",
    "        print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
    "        return result\n",
    "    return new_func"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__END_OF_DEFS__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ndarray, the base class\n",
    "\n",
    "The `ndarray` is the underlying container of numerical data. It can be thought of as micropython's own `array` object, but has a great number of extra features starting with how it can be initialised, which operations can be done on it, and which functions can accept it as an argument. One important property of an `ndarray` is that it is also a proper `micropython` iterable.\n",
    "\n",
    "The `ndarray` consists of a short header, and a pointer that holds the data. The pointer always points to a contiguous segment in memory (`numpy` is more flexible in this regard), and the header tells the interpreter, how the data from this segment is to be read out, and what the bytes mean. Some operations, e.g., `reshape`, are fast, because they do not operate on the data, they work on the header, and therefore, only a couple of bytes are manipulated, even if there are a million data entries. A more detailed exposition of how operators are implemented can be found in the section titled [Programming ulab](#Programming_ula).\n",
    "\n",
    "Since the `ndarray` is a binary container, it is also compact, meaning that it takes only a couple of bytes of extra RAM in addition to what is required for storing the numbers themselves. `ndarray`s are also type-aware, i.e., one can save RAM by specifying a data type, and using the smallest reasonable one. Five such types are defined, namely `uint8`, `int8`, which occupy a single byte of memory per datum, `uint16`, and `int16`, which occupy two bytes per datum, and `float`, which occupies four or eight bytes per datum. The precision/size of the `float` type depends on the definition of `mp_float_t`. Some platforms, e.g., the PYBD, implement `double`s, but some, e.g., the pyboard.v.11, do not. You can find out, what type of float your particular platform implements by looking at the output of the [.itemsize](#.itemsize) class property, or looking at the exact `dtype`, when you print out an array.\n",
    "\n",
    "In addition to the five above-mentioned numerical types, it is also possible to define Boolean arrays, which can be used in the indexing of data. However, Boolean arrays are really nothing but arrays of type `uint8` with an extra flag. \n",
    "\n",
    "On the following pages, we will see how one can work with `ndarray`s. Those familiar with `numpy` should find that the nomenclature and naming conventions of `numpy` are adhered to as closely as possible. We will point out the few differences, where necessary.\n",
    "\n",
    "For the sake of comparison, in addition to the `ulab` code snippets, sometimes the equivalent `numpy` code is also presented. You can find out, where the snippet is supposed to run by looking at its first line, the header of the code block."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The ndinfo function\n",
    "\n",
    "A concise summary of a couple of the properties of an `ndarray` can be printed out by calling the `ndinfo` \n",
    "function. In addition to finding out what the *shape* and *strides* of the array array, we also get the `itemsize`, as well as the type.  An interesting piece of information is the *data pointer*, which tells us, what the address of the data segment of the `ndarray` is. We will see the significance of this in the section [Slicing and indexing](#Slicing-and-indexing).\n",
    "\n",
    "Note that this function simply prints some information, but does not return anything. If you need to get a handle of the data contained in the printout, you should call the dedicated `shape`, `strides`, or `itemsize` functions directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:24:08.710325Z",
     "start_time": "2021-01-12T16:24:08.699287Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class: ndarray\n",
      "shape: (5,)\n",
      "strides: (8,)\n",
      "itemsize: 8\n",
      "data pointer: 0x7f8f6fa2e240\n",
      "type: float\n",
      "\n",
      "\n",
      "class: ndarray\n",
      "shape: (5, 5)\n",
      "strides: (5, 1)\n",
      "itemsize: 1\n",
      "data pointer: 0x7f8f6fa2e2e0\n",
      "type: uint8\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(5), dtype=np.float)\n",
    "b = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
    "np.ndinfo(a)\n",
    "print('\\n')\n",
    "np.ndinfo(b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initialising an array\n",
    "\n",
    "A new array can be created by passing either a standard micropython iterable, or another `ndarray` into the constructor."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Initialising by passing iterables\n",
    "\n",
    "If the iterable is one-dimensional, i.e., one whose elements are numbers, then a row vector will be created and returned. If the iterable is two-dimensional, i.e., one whose elements are again iterables, a matrix will be created. If the lengths of the iterables are not consistent, a `ValueError` will be raised. Iterables of different types can be mixed in the initialisation function. \n",
    "\n",
    "If the `dtype` keyword with the possible `uint8/int8/uint16/int16/float` values is supplied, the new `ndarray` will have that type, otherwise, it assumes `float` as default. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:24:21.952689Z",
     "start_time": "2021-01-12T16:24:21.938231Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t [1, 2, 3, 4, 5, 6, 7, 8]\n",
      "b:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
      "\n",
      "c:\t array([[0, 1, 2, 3, 4],\n",
      "       [20, 21, 22, 23, 24],\n",
      "       [44, 55, 66, 77, 88]], dtype=uint8)\n",
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"/dev/shm/micropython.py\", line 15, in <module>\n",
      "ValueError: iterables are not of the same length\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = [1, 2, 3, 4, 5, 6, 7, 8]\n",
    "b = np.array(a)\n",
    "\n",
    "print(\"a:\\t\", a)\n",
    "print(\"b:\\t\", b)\n",
    "\n",
    "# a two-dimensional array with mixed-type initialisers\n",
    "c = np.array([range(5), range(20, 25, 1), [44, 55, 66, 77, 88]], dtype=np.uint8)\n",
    "print(\"\\nc:\\t\", c)\n",
    "\n",
    "# and now we throw an exception\n",
    "d = np.array([range(5), range(10), [44, 55, 66, 77, 88]], dtype=np.uint8)\n",
    "print(\"\\nd:\\t\", d)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Initialising by passing arrays\n",
    "\n",
    "An `ndarray` can be initialised by supplying another array. This statement is almost trivial, since `ndarray`s are iterables themselves, though it should be pointed out that initialising through arrays is a bit faster. This statement is especially true, if the `dtype`s of the source and output arrays are the same, because then the contents can simply be copied without further ado. While type conversion is also possible, it will always be slower than straight copying."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:24:33.050654Z",
     "start_time": "2021-01-12T16:24:33.039754Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t [1, 2, 3, 4, 5, 6, 7, 8]\n",
      "\n",
      "b:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
      "\n",
      "c:\t array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
      "\n",
      "d:\t array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = [1, 2, 3, 4, 5, 6, 7, 8]\n",
    "b = np.array(a)\n",
    "c = np.array(b)\n",
    "d = np.array(b, dtype=np.uint8)\n",
    "\n",
    "print(\"a:\\t\", a)\n",
    "print(\"\\nb:\\t\", b)\n",
    "print(\"\\nc:\\t\", c)\n",
    "print(\"\\nd:\\t\", d)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that the default type of the `ndarray` is `float`. Hence, if the array is initialised from another array, type conversion will always take place, except, when the output type is specifically supplied. I.e., "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:24:39.722844Z",
     "start_time": "2021-01-12T16:24:39.709963Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
      "\n",
      "b:\t array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(5), dtype=np.uint8)\n",
    "b = np.array(a)\n",
    "print(\"a:\\t\", a)\n",
    "print(\"\\nb:\\t\", b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "will iterate over the elements in `a`, since in the assignment `b = np.array(a)`, no output type was given, therefore, `float` was assumed. On the other hand, "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:25:06.597051Z",
     "start_time": "2021-01-12T16:25:06.585511Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
      "\n",
      "b:\t array([0, 1, 2, 3, 4], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(5), dtype=np.uint8)\n",
    "b = np.array(a, dtype=np.uint8)\n",
    "print(\"a:\\t\", a)\n",
    "print(\"\\nb:\\t\", b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "will simply copy the content of `a` into `b` without any iteration, and will, therefore, be faster. Keep this in mind, whenever the output type, or performance is important."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Array initialisation functions\n",
    "\n",
    "There are nine functions that can be used for initialising an array.\n",
    "\n",
    "1. [numpy.arange](#arange)\n",
    "1. [numpy.concatenate](#concatenate)\n",
    "1. [numpy.eye](#eye)\n",
    "1. [numpy.frombuffer](#frombuffer)\n",
    "1. [numpy.full](#full)\n",
    "1. [numpy.linspace](#linspace)\n",
    "1. [numpy.logspace](#logspace)\n",
    "1. [numpy.ones](#ones)\n",
    "1. [numpy.zeros](#zeros)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### arange\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.arange.html\n",
    "\n",
    "The function returns a one-dimensional array with evenly spaced values. Takes 3 positional arguments (two are optional), and the `dtype` keyword argument. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:26:03.795728Z",
     "start_time": "2021-01-12T16:26:03.782352Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)\n",
      "array([2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)\n",
      "array([2, 5, 8], dtype=int16)\n",
      "array([2.0, 5.0, 8.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(np.arange(10))\n",
    "print(np.arange(2, 10))\n",
    "print(np.arange(2, 10, 3))\n",
    "print(np.arange(2, 10, 3, dtype=np.float))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### concatenate\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html\n",
    "\n",
    "The function joins a sequence of arrays, if they are compatible in shape, i.e., if all shapes except the one along the joining axis are equal. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:26:37.145965Z",
     "start_time": "2021-01-12T16:26:37.134350Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[0, 1, 2, 3, 4],\n",
      "       [5, 6, 7, 8, 9],\n",
      "       [10, 11, 12, 13, 14],\n",
      "       [15, 16, 17, 18, 19],\n",
      "       [20, 21, 22, 23, 24],\n",
      "       [0, 1, 2, 3, 4],\n",
      "       [5, 6, 7, 8, 9],\n",
      "       [10, 11, 12, 13, 14]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
    "b = np.array(range(15), dtype=np.uint8).reshape((3, 5))\n",
    "\n",
    "c = np.concatenate((a, b), axis=0)\n",
    "print(c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING**: `numpy` accepts arbitrary `dtype`s in the sequence of arrays, in `ulab` the `dtype`s must be identical. If you want to concatenate different types, you have to convert all arrays to the same type first. Here `b` is of `float` type, so it cannot directly be concatenated to `a`. However, if we cast the `dtype` of `b`, the concatenation works:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:26:56.120820Z",
     "start_time": "2021-01-12T16:26:56.102365Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([[0, 1, 2, 3, 4],\n",
      "       [5, 6, 7, 8, 9],\n",
      "       [10, 11, 12, 13, 14],\n",
      "       [15, 16, 17, 18, 19],\n",
      "       [20, 21, 22, 23, 24]], dtype=uint8)\n",
      "====================\n",
      "d:  array([[1, 2, 3],\n",
      "       [4, 5, 6],\n",
      "       [7, 8, 9],\n",
      "       [10, 11, 12],\n",
      "       [13, 14, 15]], dtype=uint8)\n",
      "====================\n",
      "c:  array([[1, 2, 3, 0, 1, 2, 3, 4],\n",
      "       [4, 5, 6, 5, 6, 7, 8, 9],\n",
      "       [7, 8, 9, 10, 11, 12, 13, 14],\n",
      "       [10, 11, 12, 15, 16, 17, 18, 19],\n",
      "       [13, 14, 15, 20, 21, 22, 23, 24]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(25), dtype=np.uint8).reshape((5, 5))\n",
    "b = np.array(range(15), dtype=np.float).reshape((5, 3))\n",
    "d = np.array(b+1, dtype=np.uint8)\n",
    "print('a: ', a)\n",
    "print('='*20 + '\\nd: ', d)\n",
    "c = np.concatenate((d, a), axis=1)\n",
    "print('='*20 + '\\nc: ', c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### eye\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.eye.html\n",
    "\n",
    "Another special array method is the `eye` function, whose call signature is \n",
    "\n",
    "```python\n",
    "eye(N, M, k=0, dtype=float)\n",
    "```\n",
    "where `N` (`M`) specify the dimensions of the matrix (if only `N` is supplied, then we get a square matrix, otherwise one with `M` rows, and `N` columns), and `k` is the shift of the ones (the main diagonal corresponds to `k=0`). Here are a couple of examples."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### With a single argument"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:27:08.533394Z",
     "start_time": "2021-01-12T16:27:08.518940Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[1.0, 0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 1.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 1.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 1.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0, 1.0]], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(np.eye(5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Specifying the dimensions of the matrix"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:27:34.075468Z",
     "start_time": "2021-01-12T16:27:34.064137Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[0, 0, 0, 0, 0, 0],\n",
      "       [1, 0, 0, 0, 0, 0],\n",
      "       [0, 1, 0, 0, 0, 0],\n",
      "       [0, 0, 1, 0, 0, 0]], dtype=int16)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(np.eye(4, M=6, k=-1, dtype=np.int16))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:27:42.492135Z",
     "start_time": "2021-01-12T16:27:42.477684Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[1, 0, 0, 0, 0, 0],\n",
      "       [0, 1, 0, 0, 0, 0],\n",
      "       [0, 0, 1, 0, 0, 0],\n",
      "       [0, 0, 0, 1, 0, 0]], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(np.eye(4, M=6, dtype=np.int8))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### frombuffer\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.frombuffer.html\n",
    "\n",
    "The function interprets a contiguous buffer as a one-dimensional array, and thus can be used for piping buffered data directly into an array. This method of analysing, e.g., ADC data is much more efficient than passing the ADC buffer into the `array` constructor, because `frombuffer` simply creates the `ndarray` header and blindly copies the memory segment, without inspecting the underlying data. \n",
    "\n",
    "The function takes a single positional argument, the buffer, and three keyword arguments. These are the `dtype` with a default value of `float`, the `offset`, with a default of 0, and the `count`, with a default of -1, meaning that all data are taken in."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-15T07:01:35.320458Z",
     "start_time": "2021-01-15T07:01:35.307407Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "buffer:  b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
      "a, all data read:  array([1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "b, all data with an offset:  array([3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "c, only 3 items with an offset:  array([3, 4, 5], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "buffer = b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
    "print('buffer: ', buffer)\n",
    "\n",
    "a = np.frombuffer(buffer, dtype=np.uint8)\n",
    "print('a, all data read: ', a)\n",
    "\n",
    "b = np.frombuffer(buffer, dtype=np.uint8, offset=2)\n",
    "print('b, all data with an offset: ', b)\n",
    "\n",
    "c = np.frombuffer(buffer, dtype=np.uint8, offset=2, count=3)\n",
    "print('c, only 3 items with an offset: ', c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### full\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.full.html\n",
    "\n",
    "The function returns an array of arbitrary dimension, whose elements are all equal to the second positional argument. The first argument is a tuple describing the shape of the tensor. The `dtype` keyword argument with a default value of `float` can also be supplied."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:29:11.931011Z",
     "start_time": "2021-01-12T16:29:11.915195Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[3.0, 3.0, 3.0, 3.0],\n",
      "       [3.0, 3.0, 3.0, 3.0]], dtype=float64)\n",
      "\n",
      "====================\n",
      "\n",
      "array([[3, 3, 3, 3],\n",
      "       [3, 3, 3, 3]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "# create an array with the default type\n",
    "print(np.full((2, 4), 3))\n",
    "\n",
    "print('\\n' + '='*20 + '\\n')\n",
    "# the array type is uint8 now\n",
    "print(np.full((2, 4), 3, dtype=np.uint8))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### linspace\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html\n",
    "\n",
    "This function returns an array, whose elements are uniformly spaced between the `start`, and `stop` points. The number of intervals is determined by the `num` keyword argument, whose default value is 50. With the `endpoint` keyword argument (defaults to `True`) one can include `stop` in the sequence. In addition, the `dtype` keyword can be supplied to force type conversion of the output. The default is `float`. Note that, when `dtype` is of integer type, the sequence is not necessarily evenly spaced. This is not an error, rather a consequence of rounding. (This is also the `numpy` behaviour.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:29:45.897927Z",
     "start_time": "2021-01-12T16:29:45.876325Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "default sequence:\t array([0.0, 0.2040816326530612, 0.4081632653061225, ..., 9.591836734693871, 9.795918367346932, 9.999999999999993], dtype=float64)\n",
      "num=5:\t\t\t array([0.0, 2.5, 5.0, 7.5, 10.0], dtype=float64)\n",
      "num=5:\t\t\t array([0.0, 2.0, 4.0, 6.0, 8.0], dtype=float64)\n",
      "num=5:\t\t\t array([0, 0, 1, 2, 2, 3, 4], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "# generate a sequence with defaults\n",
    "print('default sequence:\\t', np.linspace(0, 10))\n",
    "\n",
    "# num=5\n",
    "print('num=5:\\t\\t\\t', np.linspace(0, 10, num=5))\n",
    "\n",
    "# num=5, endpoint=False\n",
    "print('num=5:\\t\\t\\t', np.linspace(0, 10, num=5, endpoint=False))\n",
    "\n",
    "# num=5, endpoint=False, dtype=uint8\n",
    "print('num=5:\\t\\t\\t', np.linspace(0, 5, num=7, endpoint=False, dtype=np.uint8))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### logspace\n",
    "\n",
    "`linspace`' equivalent for logarithmically spaced data is `logspace`. This function produces a sequence of numbers, in which the quotient of consecutive numbers is constant. This is a geometric sequence.\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.logspace.html\n",
    "\n",
    "This function returns an array, whose elements are uniformly spaced between the `start`, and `stop` points. The number of intervals is determined by the `num` keyword argument, whose default value is 50. With the `endpoint` keyword argument (defaults to `True`) one can include `stop` in the sequence. In addition, the `dtype` keyword can be supplied to force type conversion of the output. The default is `float`. Note that, exactly as in `linspace`, when `dtype` is of integer type, the sequence is not necessarily evenly spaced in log space.\n",
    "\n",
    "In addition to the keyword arguments found in `linspace`, `logspace` also accepts the `base` argument. The default value is 10. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:30:44.483893Z",
     "start_time": "2021-01-12T16:30:44.466705Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "default sequence:\t array([1.0, 1.151395399326447, 1.325711365590109, ..., 754.3120063354646, 868.5113737513561, 1000.000000000004], dtype=float64)\n",
      "num=5:\t\t\t array([10.0, 1778.279410038923, 316227.766016838, 56234132.5190349, 10000000000.0], dtype=float64)\n",
      "num=5:\t\t\t array([10.0, 630.9573444801933, 39810.71705534974, 2511886.431509581, 158489319.2461114], dtype=float64)\n",
      "num=5:\t\t\t array([2.0, 6.964404506368993, 24.25146506416637, 84.44850628946524, 294.066778879241], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "# generate a sequence with defaults\n",
    "print('default sequence:\\t', np.logspace(0, 3))\n",
    "\n",
    "# num=5\n",
    "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5))\n",
    "\n",
    "# num=5, endpoint=False\n",
    "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5, endpoint=False))\n",
    "\n",
    "# num=5, endpoint=False\n",
    "print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5, endpoint=False, base=2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ones, zeros\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.zeros.html\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ones.html\n",
    "\n",
    "A couple of special arrays and matrices can easily be initialised by calling one of the `ones`, or `zeros` functions. `ones` and `zeros` follow the same pattern, and have the call signature\n",
    "\n",
    "```python\n",
    "ones(shape, dtype=float)\n",
    "zeros(shape, dtype=float)\n",
    "```\n",
    "where shape is either an integer, or a tuple specifying the shape."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-12T16:32:05.422109Z",
     "start_time": "2021-01-12T16:32:05.407921Z"
    },
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([1, 1, 1, 1, 1, 1], dtype=uint8)\n",
      "array([[0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0],\n",
      "       [0.0, 0.0, 0.0, 0.0]], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "print(np.ones(6, dtype=np.uint8))\n",
    "\n",
    "print(np.zeros((6, 4)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " When specifying the shape, make sure that the length of the tuple is not larger than the maximum dimension of your firmware."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:01:44.960353Z",
     "start_time": "2021-01-13T06:01:44.944935Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "maximum number of dimensions:  2.1.0-2D\n",
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"/dev/shm/micropython.py\", line 7, in <module>\n",
      "TypeError: too many dimensions\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "import ulab\n",
    "\n",
    "print('maximum number of dimensions: ', ulab.__version__)\n",
    "\n",
    "print(np.zeros((2, 2, 2)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Customising array printouts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`ndarray`s are pretty-printed, i.e., if the number of entries along the last axis is larger than 10 (default value), then only the first and last three entries will be printed. Also note that, as opposed to `numpy`, the printout always contains the `dtype`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:02:20.162127Z",
     "start_time": "2021-01-13T06:02:20.146219Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0.0, 1.0, 2.0, ..., 197.0, 198.0, 199.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(200))\n",
    "print(\"a:\\t\", a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### set_printoptions\n",
    "\n",
    "The default values can be overwritten by means of the `set_printoptions` function [numpy.set_printoptions](https://numpy.org/doc/1.18/reference/generated/numpy.set_printoptions.html), which accepts two keywords arguments, the `threshold`, and the `edgeitems`. The first of these arguments determines the length of the longest array that will be printed in full, while the second is the number of items that will be printed on the left and right hand side of the ellipsis, if the array is longer than `threshold`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:02:42.073823Z",
     "start_time": "2021-01-13T06:02:42.057424Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a printed with defaults:\t array([0.0, 1.0, 2.0, ..., 17.0, 18.0, 19.0], dtype=float64)\n",
      "\n",
      "a printed in full:\t\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0], dtype=float64)\n",
      "\n",
      "a truncated with 2 edgeitems:\t array([0.0, 1.0, ..., 18.0, 19.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(20))\n",
    "print(\"a printed with defaults:\\t\", a)\n",
    "\n",
    "np.set_printoptions(threshold=200)\n",
    "print(\"\\na printed in full:\\t\\t\", a)\n",
    "\n",
    "np.set_printoptions(threshold=10, edgeitems=2)\n",
    "print(\"\\na truncated with 2 edgeitems:\\t\", a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### get_printoptions\n",
    "\n",
    "The set value of the `threshold` and `edgeitems` can be retrieved by calling the `get_printoptions` function with no arguments. The function returns a *dictionary* with two keys."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:02:51.383653Z",
     "start_time": "2021-01-13T06:02:51.372551Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'threshold': 100, 'edgeitems': 20}\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "np.set_printoptions(threshold=100, edgeitems=20)\n",
    "print(np.get_printoptions())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Methods and properties of ndarrays\n",
    "\n",
    "Arrays have several *properties* that can queried, and some methods that can be called. With the exception of the flatten and transpose operators, properties return an object that describe some feature of the array, while the methods return a new array-like object. \n",
    "\n",
    "1. [.byteswap](#.byteswap)\n",
    "1. [.copy](#.copy)\n",
    "1. [.dtype](#.dtype)\n",
    "1. [.flatten](#.flatten)\n",
    "1. [.itemsize](#.itemsize)\n",
    "1. [.reshape](#.reshape)\n",
    "1. [.shape](#.shape)\n",
    "1. [.size](#.size)\n",
    "1. [.transpose](.#transpose)\n",
    "1. [.sort](#.sort)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .byteswap\n",
    "\n",
    "`numpy` https://numpy.org/doc/stable/reference/generated/numpy.char.chararray.byteswap.html\n",
    "\n",
    "The method takes a single keyword argument, `inplace`, with values `True` or `False`, and swaps the bytes in the array.  If `inplace = False`, a new `ndarray` is returned, otherwise the original values are overwritten.\n",
    "\n",
    "The `frombuffer` function is a convenient way of receiving data from peripheral devices that work with buffers. However, it is not guaranteed that the byte order (in other words, the _endianness_) of the peripheral device matches that of the microcontroller. The `.byteswap` method makes it possible to change the endianness of the incoming data stream.\n",
    "\n",
    "Obviously, byteswapping makes sense only for those cases, when a datum occupies more than one byte, i.e., for the `uint16`, `int16`, and `float` `dtype`s. When `dtype` is either `uint8`, or `int8`, the method simply returns a view or copy of self, depending upon the value of `inplace`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-02-15T16:06:20.409727Z",
     "start_time": "2021-02-15T16:06:20.398057Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "buffer:  b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
      "a:  array([513, 1027, 1541, 2055], dtype=uint16)\n",
      "b:  array([258, 772, 1286, 1800], dtype=uint16)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "buffer = b'\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08'\n",
    "print('buffer: ', buffer)\n",
    "\n",
    "a = np.frombuffer(buffer, dtype=np.uint16)\n",
    "print('a: ', a)\n",
    "b = a.byteswap()\n",
    "print('b: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .copy\n",
    "\n",
    "The `.copy` method creates a new *deep copy* of an array, i.e., the entries of the source array are *copied* into the target array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:02:58.898485Z",
     "start_time": "2021-01-13T06:02:58.878864Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([1, 2, 3, 4], dtype=int8)\n",
      "====================\n",
      "b:  array([1, 2, 3, 4], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "b = a.copy()\n",
    "print('a: ', a)\n",
    "print('='*20)\n",
    "print('b: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .dtype\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.dtype.htm\n",
    "\n",
    "The `.dtype` property returns the `dtype` of an array. This can then be used for initialising another array with the matching type. `ulab` implements two versions of `dtype`; one that is `numpy`-like, i.e., one, which returns a `dtype` object, and one that is significantly cheaper in terms of flash space, but does not define a the `dtype` object, and returns a single character (number) instead. \n",
    "\n",
    "**WARNING**: in `circuitpython`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-11-02T17:16:12.818777Z",
     "start_time": "2020-11-02T17:16:12.807147Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([1, 2, 3, 4], dtype=int8)\n",
      "dtype of a:  dtype('int8')\n",
      "\n",
      "b:  array([5, 6, 7], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "b = np.array([5, 6, 7], dtype=a.dtype)\n",
    "print('a: ', a)\n",
    "print('dtype of a: ', a.dtype)\n",
    "print('\\nb: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING:** in `micropython`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:05:06.183009Z",
     "start_time": "2021-01-13T06:05:06.167855Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([1, 2, 3, 4], dtype=int8)\n",
      "dtype of a:  dtype('int8')\n",
      "\n",
      "b:  array([5, 6, 7], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "b = np.array([5, 6, 7], dtype=a.dtype())\n",
    "print('a: ', a)\n",
    "print('dtype of a: ', a.dtype())\n",
    "print('\\nb: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If the `ulab.h` header file sets the pre-processor constant `ULAB_HAS_DTYPE_OBJECT` to 0 as\n",
    "\n",
    "```c\n",
    "#define ULAB_HAS_DTYPE_OBJECT               (0)\n",
    "```\n",
    "then the output of the previous snippet will be"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-11-02T20:36:23.099166Z",
     "start_time": "2020-11-02T20:36:23.088586Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([1, 2, 3, 4], dtype=int8)\n",
      "dtype of a:  98\n",
      "\n",
      "b:  array([5, 6, 7], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "b = np.array([5, 6, 7], dtype=a.dtype())\n",
    "print('a: ', a)\n",
    "print('dtype of a: ', a.dtype())\n",
    "print('\\nb: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here 98 is nothing but the ASCII value of the character `b`, which is the type code for signed 8-bit integers. The object definition adds around 600 bytes to the firmware."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .flatten\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flatten.htm\n",
    "\n",
    "`.flatten` returns the flattened array. The array can be flattened in `C` style (i.e., moving along the last axis in the tensor), or in `fortran` style (i.e., moving along the first axis in the tensor)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:07:16.735771Z",
     "start_time": "2021-01-13T06:07:16.723514Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a: \t\t array([1, 2, 3, 4], dtype=int8)\n",
      "a flattened: \t array([1, 2, 3, 4], dtype=int8)\n",
      "\n",
      "b: array([[1, 2, 3],\n",
      "       [4, 5, 6]], dtype=int8)\n",
      "b flattened (C): \t array([1, 2, 3, 4, 5, 6], dtype=int8)\n",
      "b flattened (F): \t array([1, 4, 2, 5, 3, 6], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "print(\"a: \\t\\t\", a)\n",
    "print(\"a flattened: \\t\", a.flatten())\n",
    "\n",
    "b = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int8)\n",
    "print(\"\\nb:\", b)\n",
    "\n",
    "print(\"b flattened (C): \\t\", b.flatten())\n",
    "print(\"b flattened (F): \\t\", b.flatten(order='F'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .itemsize\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.itemsize.html\n",
    "\n",
    "The `.itemsize` method (property) returns an integer with the size of elements in the array.\n",
    "\n",
    "**WARNING:** In `circuitpython`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:07:49.080817Z",
     "start_time": "2021-01-13T06:07:49.065749Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3], dtype=int8)\n",
      "itemsize of a: 1\n",
      "\n",
      "b:\n",
      " array([[1.0, 2.0],\n",
      "       [3.0, 4.0]], dtype=float64)\n",
      "itemsize of b: 8\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"itemsize of a:\", a.itemsize\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.float)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"itemsize of b:\", b.itemsize"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING:** In `micropython`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:08:01.630382Z",
     "start_time": "2021-01-13T06:08:01.619318Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3], dtype=int8)\n",
      "itemsize of a: <bound_method 7fdc008692c0 array([1, 2, 3], dtype=int8).<function>>\n",
      "\n",
      "b:\n",
      " array([[1.0, 2.0],\n",
      "       [3.0, 4.0]], dtype=float64)\n",
      "itemsize of b: 8\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"itemsize of a:\", a.itemsize)\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.float)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"itemsize of b:\", b.itemsize())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .reshape\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html\n",
    "\n",
    "`reshape` re-writes the shape properties of an `ndarray`, but the array will not be modified in any other way. The function takes a single 2-tuple with two integers as its argument. The 2-tuple should specify the desired number of rows and columns. If the new shape is not consistent with the old, a `ValueError` exception will be raised."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:08:12.234490Z",
     "start_time": "2021-01-13T06:08:12.217652Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a (4 by 4): array([[1, 2, 3, 4],\n",
      "       [5, 6, 7, 8],\n",
      "       [9, 10, 11, 12],\n",
      "       [13, 14, 15, 16]], dtype=uint8)\n",
      "a (2 by 8): array([[1, 2, 3, 4, 5, 6, 7, 8],\n",
      "       [9, 10, 11, 12, 13, 14, 15, 16]], dtype=uint8)\n",
      "a (1 by 16): array([[1, 2, 3, ..., 14, 15, 16]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], dtype=np.uint8)\n",
    "print('a (4 by 4):', a)\n",
    "print('a (2 by 8):', a.reshape((2, 8)))\n",
    "print('a (1 by 16):', a.reshape((1, 16)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .shape\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html\n",
    "\n",
    "The `.shape` method (property) returns a tuple with the length of the array in along each dimension. \n",
    "\n",
    "**WARNING:** In `circuitpython`, you can call the method as a property, i.e., "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:08:50.479850Z",
     "start_time": "2021-01-13T06:08:50.464741Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3, 4], dtype=int8)\n",
      "shape of a: (4,)\n",
      "\n",
      "b:\n",
      " array([[1, 2],\n",
      "       [3, 4]], dtype=int8)\n",
      "shape of b: (2, 2)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"shape of a:\", a.shape)\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"shape of b:\", b.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING:** On the other hand, since properties are not implemented in `micropython`, there you would call the method as a function, i.e., "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:08:36.531124Z",
     "start_time": "2021-01-13T06:08:36.515040Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3, 4], dtype=int8)\n",
      "shape of a: (4,)\n",
      "\n",
      "b:\n",
      " array([[1, 2],\n",
      "       [3, 4]], dtype=int8)\n",
      "shape of b: (2, 2)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"shape of a:\", a.shape())\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"shape of b:\", b.shape())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .size\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.size.html\n",
    "\n",
    "The `.size` method (property) returns an integer with the number of elements in the array. \n",
    "\n",
    "**WARNING:** In `circuitpython`, the `numpy` nomenclature applies, i.e., "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-02-11T06:32:22.721112Z",
     "start_time": "2020-02-11T06:32:22.713111Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3], dtype=int8)\n",
      "size of a: 3\n",
      "\n",
      "b:\n",
      " array([[1, 2],\n",
      "\t [3, 4]], dtype=int8)\n",
      "size of b: 4\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"size of a:\", a.size)\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"size of b:\", b.size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING:** In `micropython`, `size` is a method, i.e., "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:09:20.062380Z",
     "start_time": "2021-01-13T06:09:20.045307Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1, 2, 3], dtype=int8)\n",
      "size of a: 3\n",
      "\n",
      "b:\n",
      " array([[1, 2],\n",
      "       [3, 4]], dtype=int8)\n",
      "size of b: 4\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3], dtype=np.int8)\n",
    "print(\"a:\\n\", a)\n",
    "print(\"size of a:\", a.size())\n",
    "\n",
    "b= np.array([[1, 2], [3, 4]], dtype=np.int8)\n",
    "print(\"\\nb:\\n\", b)\n",
    "print(\"size of b:\", b.size())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .tobytes\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tobytes.html\n",
    "\n",
    "The `.tobytes` method can be used for acquiring a handle of the underlying data pointer of an array, and it returns a new `bytearray` that can be fed into any method that can accep a `bytearray`, e.g., ADC data can be buffered into this `bytearray`, or the `bytearray` can be fed into a DAC. Since the `bytearray` is really nothing but the bare data container of the array, any manipulation on the `bytearray` automatically modifies the array itself.\n",
    "\n",
    "Note that the method raises a `ValueError` exception, if the array is not dense (i.e., it has already been sliced)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:09:57.262071Z",
     "start_time": "2021-01-13T06:09:57.250519Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([0, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
      "b:  bytearray(b'\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07')\n",
      "====================\n",
      "b:  bytearray(b'\\r\\x01\\x02\\x03\\x04\\x05\\x06\\x07')\n",
      "a:  array([13, 1, 2, 3, 4, 5, 6, 7], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(8), dtype=np.uint8)\n",
    "print('a: ', a)\n",
    "b = a.tobytes()\n",
    "print('b: ', b)\n",
    "\n",
    "# modify b\n",
    "b[0] = 13\n",
    "\n",
    "print('='*20)\n",
    "print('b: ', b)\n",
    "print('a: ', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .transpose\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.transpose.html\n",
    "\n",
    "Returns the transposed array. Only defined, if the number of maximum dimensions is larger than 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 384,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-19T08:39:11.844987Z",
     "start_time": "2019-10-19T08:39:11.828099Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[1, 2, 3],\n",
      "\t [4, 5, 6],\n",
      "\t [7, 8, 9],\n",
      "\t [10, 11, 12]], dtype=uint8)\n",
      "shape of a: (4, 3)\n",
      "\n",
      "transpose of a:\n",
      " array([[1, 4, 7, 10],\n",
      "\t [2, 5, 8, 11],\n",
      "\t [3, 6, 9, 12]], dtype=uint8)\n",
      "shape of a: (3, 4)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.uint8)\n",
    "print('a:\\n', a)\n",
    "print('shape of a:', a.shape())\n",
    "a.transpose()\n",
    "print('\\ntranspose of a:\\n', a)\n",
    "print('shape of a:', a.shape())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### .sort\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.sort.html\n",
    "\n",
    "In-place sorting of an `ndarray`. For a more detailed exposition, see [sort](#sort)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:11:20.989109Z",
     "start_time": "2021-01-13T06:11:20.972842Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "a:\n",
      " array([[1, 12, 3, 0],\n",
      "       [5, 3, 4, 1],\n",
      "       [9, 11, 1, 8],\n",
      "       [7, 10, 0, 1]], dtype=uint8)\n",
      "\n",
      "a sorted along vertical axis:\n",
      " array([[1, 3, 0, 0],\n",
      "       [5, 10, 1, 1],\n",
      "       [7, 11, 3, 1],\n",
      "       [9, 12, 4, 8]], dtype=uint8)\n",
      "\n",
      "a sorted along horizontal axis:\n",
      " array([[0, 1, 3, 12],\n",
      "       [1, 3, 4, 5],\n",
      "       [1, 8, 9, 11],\n",
      "       [0, 1, 7, 10]], dtype=uint8)\n",
      "\n",
      "flattened a sorted:\n",
      " array([0, 0, 1, ..., 10, 11, 12], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
    "print('\\na:\\n', a)\n",
    "a.sort(axis=0)\n",
    "print('\\na sorted along vertical axis:\\n', a)\n",
    "\n",
    "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
    "a.sort(axis=1)\n",
    "print('\\na sorted along horizontal axis:\\n', a)\n",
    "\n",
    "a = np.array([[1, 12, 3, 0], [5, 3, 4, 1], [9, 11, 1, 8], [7, 10, 0, 1]], dtype=np.uint8)\n",
    "a.sort(axis=None)\n",
    "print('\\nflattened a sorted:\\n', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Unary operators\n",
    "\n",
    "With the exception of `len`, which returns a single number, all unary operators manipulate the underlying data element-wise. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### len\n",
    "\n",
    "This operator takes a single argument, the array, and returns either the length of the first axis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:11:49.266192Z",
     "start_time": "2021-01-13T06:11:49.255493Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([1, 2, 3, 4, 5], dtype=uint8)\n",
      "length of a:  5\n",
      "shape of a:  (5,)\n",
      "\n",
      "b:\t array([[0, 1, 2, 3, 4],\n",
      "       [0, 1, 2, 3, 4],\n",
      "       [0, 1, 2, 3, 4],\n",
      "       [0, 1, 2, 3, 4]], dtype=uint8)\n",
      "length of b:  2\n",
      "shape of b:  (4, 5)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
    "b = np.array([range(5), range(5), range(5), range(5)], dtype=np.uint8)\n",
    "\n",
    "print(\"a:\\t\", a)\n",
    "print(\"length of a: \", len(a))\n",
    "print(\"shape of a: \", a.shape())\n",
    "print(\"\\nb:\\t\", b)\n",
    "print(\"length of b: \", len(b))\n",
    "print(\"shape of b: \", b.shape())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " The number returned by `len` is also the length of the iterations, when the array supplies the elements for an iteration (see later)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### invert\n",
    "\n",
    "The function is defined for integer data types (`uint8`, `int8`, `uint16`, and `int16`) only, takes a single argument, and returns the element-by-element, bit-wise inverse of the array. If a `float` is supplied, the function raises a `ValueError` exception.\n",
    "\n",
    "With signed integers (`int8`, and `int16`), the results might be unexpected, as in the example below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 98,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T13:16:16.754210Z",
     "start_time": "2019-10-11T13:16:16.735618Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t array([0, -1, -100], dtype=int8)\n",
      "inverse of a:\t array([-1, 0, 99], dtype=int8)\n",
      "\n",
      "a:\t\t array([0, 1, 254, 255], dtype=uint8)\n",
      "inverse of a:\t array([255, 254, 1, 0], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([0, -1, -100], dtype=np.int8)\n",
    "print(\"a:\\t\\t\", a)\n",
    "print(\"inverse of a:\\t\", ~a)\n",
    "\n",
    "a = np.array([0, 1, 254, 255], dtype=np.uint8)\n",
    "print(\"\\na:\\t\\t\", a)\n",
    "print(\"inverse of a:\\t\", ~a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### abs\n",
    "\n",
    "This function takes a single argument, and returns the element-by-element absolute value of the array. When the data type is unsigned (`uint8`, or `uint16`), a copy of the array will be returned immediately, and no calculation takes place."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T13:05:43.926821Z",
     "start_time": "2019-10-11T13:05:43.912629Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t\t  array([0, -1, -100], dtype=int8)\n",
      "absolute value of a:\t  array([0, 1, 100], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([0, -1, -100], dtype=np.int8)\n",
    "print(\"a:\\t\\t\\t \", a)\n",
    "print(\"absolute value of a:\\t \", abs(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### neg\n",
    "\n",
    "This operator takes a single argument, and changes the sign of each element in the array. Unsigned values are wrapped. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 99,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T13:17:00.946009Z",
     "start_time": "2019-10-11T13:17:00.927264Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t array([10, -1, 1], dtype=int8)\n",
      "negative of a:\t array([-10, 1, -1], dtype=int8)\n",
      "\n",
      "b:\t\t array([0, 100, 200], dtype=uint8)\n",
      "negative of b:\t array([0, 156, 56], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([10, -1, 1], dtype=np.int8)\n",
    "print(\"a:\\t\\t\", a)\n",
    "print(\"negative of a:\\t\", -a)\n",
    "\n",
    "b = np.array([0, 100, 200], dtype=np.uint8)\n",
    "print(\"\\nb:\\t\\t\", b)\n",
    "print(\"negative of b:\\t\", -b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### pos\n",
    "\n",
    "This function takes a single argument, and simply returns a copy of the array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-11T13:09:15.965662Z",
     "start_time": "2019-10-11T13:09:15.945461Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t array([10, -1, 1], dtype=int8)\n",
      "positive of a:\t array([10, -1, 1], dtype=int8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([10, -1, 1], dtype=np.int8)\n",
    "print(\"a:\\t\\t\", a)\n",
    "print(\"positive of a:\\t\", +a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Binary operators\n",
    "\n",
    "`ulab` implements the `+`, `-`, `*`, `/`, `**`, `<`, `>`, `<=`, `>=`, `==`, `!=`, `+=`, `-=`, `*=`, `/=`, `**=` binary operators that work element-wise. Broadcasting is available, meaning that the two operands do not even have to have the same shape. If the lengths along the respective axes are equal, or one of them is 1, or the axis is missing, the element-wise operation can still be carried out. \n",
    "A thorough explanation of broadcasting can be found under https://numpy.org/doc/stable/user/basics.broadcasting.html. \n",
    "\n",
    "**WARNING**: note that relational operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) should have the `ndarray` on their left hand side, when compared to scalars. This means that the following works"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:12:30.802935Z",
     "start_time": "2021-01-13T06:12:30.786069Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([False, False, True], dtype=bool)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "print(a > 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "while the equivalent statement, `2 < a`, will raise a `TypeError` exception:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:12:51.262197Z",
     "start_time": "2021-01-13T06:12:51.244206Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"/dev/shm/micropython.py\", line 5, in <module>\n",
      "TypeError: unsupported types for __lt__: 'int', 'ndarray'\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "print(2 < a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING:** `circuitpython` users should use the `equal`, and `not_equal` operators instead of `==`, and `!=`. See the section on [array comparison](#Comparison-of-arrays) for details."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Upcasting\n",
    "\n",
    "Binary operations require special attention, because two arrays with different typecodes can be the operands of an operation, in which case it is not trivial, what the typecode of the result is. This decision on the result's typecode is called upcasting. Since the number of typecodes in `ulab` is significantly smaller than in `numpy`, we have to define new upcasting rules. Where possible, I followed `numpy`'s conventions. \n",
    "\n",
    "`ulab` observes the following upcasting rules:\n",
    "\n",
    "1. Operations on two `ndarray`s of the same `dtype` preserve their `dtype`, even when the results overflow.\n",
    "\n",
    "2. if either of the operands is a float, the result is automatically a float\n",
    "\n",
    "3. When one of the operands is a scalar, it will internally be turned into a single-element `ndarray` with the *smallest* possible `dtype`. Thus, e.g., if the scalar is 123, it will be converted into an array of `dtype` `uint8`, while -1000 will be converted into `int16`. An `mp_obj_float`, will always be promoted to `dtype` `float`. Other micropython types (e.g., lists, tuples, etc.) raise a `TypeError` exception. \n",
    "\n",
    "4. \n",
    "    \n",
    "| left hand side | right hand side | ulab result | numpy result |\n",
    "|----------------|-----------------|-------------|--------------|\n",
    "|`uint8`         |`int8`           |`int16`      |`int16`       |\n",
    "|`uint8`         |`int16`          |`int16`      |`int16`       |\n",
    "|`uint8`         |`uint16`         |`uint16`     |`uint16`      |\n",
    "|`int8`          |`int16`          |`int16`      |`int16`       | \n",
    "|`int8`          |`uint16`         |`uint16`     |`int32`       |\n",
    "|`uint16`        |`int16`          |`float`      |`int32`       |\n",
    "    \n",
    "Note that the last two operations are promoted to `int32` in `numpy`.\n",
    "    \n",
    "**WARNING:** Due to the lower number of available data types, the upcasting rules of `ulab` are slightly different to those of `numpy`. Watch out for this, when porting code!\n",
    "\n",
    "Upcasting can be seen in action in the following snippet:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:13:23.026904Z",
     "start_time": "2021-01-13T06:13:23.009315Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([1, 2, 3, 4], dtype=uint8)\n",
      "b:\t array([1, 2, 3, 4], dtype=int8)\n",
      "a+b:\t array([2, 4, 6, 8], dtype=int16)\n",
      "\n",
      "a:\t array([1, 2, 3, 4], dtype=uint8)\n",
      "c:\t array([1.0, 2.0, 3.0, 4.0], dtype=float64)\n",
      "a*c:\t array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4], dtype=np.uint8)\n",
    "b = np.array([1, 2, 3, 4], dtype=np.int8)\n",
    "print(\"a:\\t\", a)\n",
    "print(\"b:\\t\", b)\n",
    "print(\"a+b:\\t\", a+b)\n",
    "\n",
    "c = np.array([1, 2, 3, 4], dtype=np.float)\n",
    "print(\"\\na:\\t\", a)\n",
    "print(\"c:\\t\", c)\n",
    "print(\"a*c:\\t\", a*c)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Benchmarks\n",
    "\n",
    "The following snippet compares the performance of binary operations to a possible implementation in python. For the time measurement, we will take the following snippet from the micropython manual:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T06:39:52.225256Z",
     "start_time": "2020-05-07T06:39:52.194691Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "import utime\n",
    "\n",
    "def timeit(f, *args, **kwargs):\n",
    "    func_name = str(f).split(' ')[1]\n",
    "    def new_func(*args, **kwargs):\n",
    "        t = utime.ticks_us()\n",
    "        result = f(*args, **kwargs)\n",
    "        print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
    "        return result\n",
    "    return new_func"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 490,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-10-19T13:23:45.432395Z",
     "start_time": "2019-10-19T13:23:45.344021Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "python add:\n",
      "execution time:  10051  us\n",
      "\n",
      "python multiply:\n",
      "execution time:  14175  us\n",
      "\n",
      "ulab add:\n",
      "execution time:  222  us\n",
      "\n",
      "ulab multiply:\n",
      "execution time:  213  us\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "@timeit\n",
    "def py_add(a, b):\n",
    "    return [a[i]+b[i] for i in range(1000)]\n",
    "\n",
    "@timeit\n",
    "def py_multiply(a, b):\n",
    "    return [a[i]*b[i] for i in range(1000)]\n",
    "\n",
    "@timeit\n",
    "def ulab_add(a, b):\n",
    "    return a + b\n",
    "\n",
    "@timeit\n",
    "def ulab_multiply(a, b):\n",
    "    return a * b\n",
    "\n",
    "a = [0.0]*1000\n",
    "b = range(1000)\n",
    "\n",
    "print('python add:')\n",
    "py_add(a, b)\n",
    "\n",
    "print('\\npython multiply:')\n",
    "py_multiply(a, b)\n",
    "\n",
    "a = np.linspace(0, 10, num=1000)\n",
    "b = np.ones(1000)\n",
    "\n",
    "print('\\nulab add:')\n",
    "ulab_add(a, b)\n",
    "\n",
    "print('\\nulab multiply:')\n",
    "ulab_multiply(a, b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The python implementation above is not perfect, and certainly, there is much room for improvement. However, the factor of 50 difference in execution time is very spectacular. This is nothing but a consequence of the fact that the `ulab` functions run `C` code, with very little python overhead. The factor of 50 appears to be quite universal: the FFT routine obeys similar scaling (see [Speed of FFTs](#Speed-of-FFTs)), and this number came up with font rendering, too: [fast font rendering on graphical displays](https://forum.micropython.org/viewtopic.php?f=15&t=5815&p=33362&hilit=ufont#p33383)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparison operators\n",
    "\n",
    "The smaller than, greater than, smaller or equal, and greater or equal operators return a vector of Booleans indicating the positions (`True`), where the condition is satisfied. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 99,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-17T15:08:38.673585Z",
     "start_time": "2020-10-17T15:08:38.659225Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([True, True, True, True, False, False, False, False], dtype=bool)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)\n",
    "print(a < 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**WARNING**: at the moment, due to `micropython`'s implementation details, the `ndarray` must be on the left hand side of the relational operators.\n",
    "\n",
    "That is, while `a < 5` and `5 > a` have the same meaning, the following code will not work:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"/dev/shm/micropython.py\", line 5, in <module>\n",
      "TypeError: unsupported types for __gt__: 'int', 'ndarray'\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "import ulab as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.uint8)\n",
    "print(5 > a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Iterating over arrays\n",
    "\n",
    "`ndarray`s are iterable, which means that their elements can also be accessed as can the elements of a list, tuple, etc. If the array is one-dimensional, the iterator returns scalars, otherwise a new reduced-dimensional *view* is created and returned."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T06:14:11.756254Z",
     "start_time": "2021-01-13T06:14:11.742246Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([1, 2, 3, 4, 5], dtype=uint8)\n",
      "element 0 in a: 1\n",
      "element 1 in a: 2\n",
      "element 2 in a: 3\n",
      "element 3 in a: 4\n",
      "element 4 in a: 5\n",
      "\n",
      "b:\t array([[0, 1, 2, 3, 4],\n",
      "       [10, 11, 12, 13, 14],\n",
      "       [20, 21, 22, 23, 24],\n",
      "       [30, 31, 32, 33, 34]], dtype=uint8)\n",
      "element 0 in b: array([0, 1, 2, 3, 4], dtype=uint8)\n",
      "element 1 in b: array([10, 11, 12, 13, 14], dtype=uint8)\n",
      "element 2 in b: array([20, 21, 22, 23, 24], dtype=uint8)\n",
      "element 3 in b: array([30, 31, 32, 33, 34], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3, 4, 5], dtype=np.uint8)\n",
    "b = np.array([range(5), range(10, 15, 1), range(20, 25, 1), range(30, 35, 1)], dtype=np.uint8)\n",
    "\n",
    "print(\"a:\\t\", a)\n",
    "\n",
    "for i, _a in enumerate(a):\n",
    "    print(\"element %d in a:\"%i, _a)\n",
    "    \n",
    "print(\"\\nb:\\t\", b)\n",
    "\n",
    "for i, _b in enumerate(b):\n",
    "    print(\"element %d in b:\"%i, _b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Slicing and indexing\n",
    "\n",
    "\n",
    "### Views vs. copies\n",
    "\n",
    "`numpy` has a very important concept called *views*, which is a powerful extension of `python`'s own notion of slicing. Slices are special python objects of the form\n",
    "\n",
    "```python\n",
    "slice = start:end:stop\n",
    "```\n",
    "\n",
    "where `start`, `end`, and `stop` are (not necessarily non-negative) integers. Not all of these three numbers must be specified in an index, in fact, all three of them can be missing. The interpreter takes care of filling in the missing values. (Note that slices cannot be defined in this way, only there, where an index is expected.) For a good explanation on how slices work in python, you can read the stackoverflow question https://stackoverflow.com/questions/509211/understanding-slice-notation.\n",
    "\n",
    "In order to see what slicing does, let us take the string `a = '012345679'`! We can extract every second character by creating the slice `::2`, which is equivalent to `0:len(a):2`, i.e., increments the character pointer by 2 starting from 0, and traversing the string up to the very end."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T05:26:17.758735Z",
     "start_time": "2020-10-12T05:26:17.748487Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'02468'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "string = '0123456789'\n",
    "string[::2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we can do the same with numerical arrays."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T05:25:49.352435Z",
     "start_time": "2020-10-12T05:25:49.339452Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)\n",
      "a[::2]:\t array([0, 2, 4, 6, 8], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(10), dtype=np.uint8)\n",
    "print('a:\\t', a)\n",
    "\n",
    "print('a[::2]:\\t', a[::2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This looks similar to `string` above, but there is a very important difference that is not so obvious. Namely, `string[::2]` produces a partial copy of `string`, while `a[::2]` only produces a *view* of `a`. What this means is that `a`, and `a[::2]` share their data, and the only difference between the two is, how the data are read out. In other words, internally, `a[::2]` has the same data pointer as `a`. We can easily convince ourselves that this is indeed the case by calling the [ndinfo](#The_ndinfo_function) function: the *data pointer* entry is the same in the two printouts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-16T18:43:07.480791Z",
     "start_time": "2020-10-16T18:43:07.471473Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8) \n",
      "\n",
      "class: ndarray\n",
      "shape: (10,)\n",
      "strides: (1,)\n",
      "itemsize: 1\n",
      "data pointer: 0x7ff6c6193220\n",
      "type: uint8\n",
      "\n",
      "====================\n",
      "a[::2]:  array([0, 2, 4, 6, 8], dtype=uint8) \n",
      "\n",
      "class: ndarray\n",
      "shape: (5,)\n",
      "strides: (2,)\n",
      "itemsize: 1\n",
      "data pointer: 0x7ff6c6193220\n",
      "type: uint8\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(10), dtype=np.uint8)\n",
    "print('a: ', a, '\\n')\n",
    "np.ndinfo(a)\n",
    "print('\\n' + '='*20)\n",
    "print('a[::2]: ', a[::2], '\\n')\n",
    "np.ndinfo(a[::2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you are still a bit confused about the meaning of *views*, the section [Slicing and assigning to slices](#Slicing-and-assigning-to-slices) should clarify the issue."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Indexing\n",
    "\n",
    "The simplest form of indexing is specifying a single integer between the square brackets as in "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T18:31:45.485584Z",
     "start_time": "2020-10-12T18:31:45.464551Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)\n",
      "the first, and last element of a:\n",
      " 0 9\n",
      "the second, and last but one element of a:\n",
      " 1 8\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(10), dtype=np.uint8)\n",
    "print(\"a: \", a)\n",
    "print(\"the first, and last element of a:\\n\", a[0], a[-1])\n",
    "print(\"the second, and last but one element of a:\\n\", a[1], a[-2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Indexing can be applied to higher-dimensional tensors, too. When the length of the indexing sequences is smaller than the number of dimensions, a new *view* is returned, otherwise, we get a single number."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T18:26:12.783883Z",
     "start_time": "2020-10-12T18:26:12.770180Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[0, 1, 2],\n",
      "\t[3, 4, 5],\n",
      "\t[6, 7, 8]], dtype=uint8)\n",
      "a[0]:\n",
      " array([[0, 1, 2]], dtype=uint8)\n",
      "a[1,1]:  4\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.uint8).reshape((3, 3))\n",
    "print(\"a:\\n\", a)\n",
    "print(\"a[0]:\\n\", a[0])\n",
    "print(\"a[1,1]: \", a[1,1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Indices can also be a list of Booleans. By using a Boolean list, we can select those elements of an array that satisfy a specific condition. At the moment, such indexing is defined for row vectors only; when the rank of the tensor is higher than 1, the function raises a `NotImplementedError` exception, though this will be rectified in a future version of `ulab`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T17:34:34.105614Z",
     "start_time": "2020-10-12T17:34:34.094017Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float)\n",
      "a < 5:\t array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.float)\n",
    "print(\"a:\\t\", a)\n",
    "print(\"a < 5:\\t\", a[a < 5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Indexing with Boolean arrays can take more complicated expressions. This is a very concise way of comparing two vectors, e.g.:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T18:03:38.846377Z",
     "start_time": "2020-10-12T18:03:38.826689Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t array([0, 1, 2, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "\n",
      "a**2:\t array([0, 1, 4, 9, 16, 25, 36, 49, 64], dtype=uint16)\n",
      "\n",
      "b:\t array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=uint8)\n",
      "\n",
      "100*sin(b):\t array([-75.68024953079282, -75.68024953079282, -75.68024953079282, 14.11200080598672, 14.11200080598672, 14.11200080598672, 42.01670368266409, 42.01670368266409, 42.01670368266409], dtype=float)\n",
      "\n",
      "a[a*a > np.sin(b)*100.0]:\t array([0, 1, 2, 4, 5, 7, 8], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.uint8)\n",
    "b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)\n",
    "print(\"a:\\t\", a)\n",
    "print(\"\\na**2:\\t\", a*a)\n",
    "print(\"\\nb:\\t\", b)\n",
    "print(\"\\n100*sin(b):\\t\", np.sin(b)*100.0)\n",
    "print(\"\\na[a*a > np.sin(b)*100.0]:\\t\", a[a*a > np.sin(b)*100.0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Boolean indices can also be used in assignments, if the array is one-dimensional. The following example replaces the data in an array, wherever some condition is fulfilled."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-13T16:14:21.055356Z",
     "start_time": "2020-10-13T16:14:21.035329Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([0, 1, 2], dtype=uint8)\n",
      "array([123, 123, 123, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.uint8)\n",
    "b = np.array(range(9)) + 12\n",
    "\n",
    "print(a[b < 15])\n",
    "\n",
    "a[b < 15] = 123\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On the right hand side of the assignment we can even have another array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-13T16:14:10.054210Z",
     "start_time": "2020-10-13T16:14:10.039523Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([0, 1, 2], dtype=uint8) array([12.0, 13.0, 14.0], dtype=float)\n",
      "array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array(range(9), dtype=np.uint8)\n",
    "b = np.array(range(9)) + 12\n",
    "\n",
    "print(a[b < 15], b[b < 15])\n",
    "\n",
    "a[b < 15] = b[b < 15]\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Slicing and assigning to slices\n",
    "\n",
    "You can also generate sub-arrays by specifying slices as the index of an array. Slices are special python objects of the form "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T17:38:15.975404Z",
     "start_time": "2020-10-12T17:38:15.955070Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[1, 2, 3],\n",
      "\t[4, 5, 6],\n",
      "\t[7, 8, 9]], dtype=uint8)\n",
      "\n",
      "a[0]:\n",
      " array([[1, 2, 3]], dtype=uint8)\n",
      "\n",
      "a[0,:2]:\n",
      " array([[1, 2]], dtype=uint8)\n",
      "\n",
      "a[:,0]:\n",
      " array([[1],\n",
      "\t[4],\n",
      "\t[7]], dtype=uint8)\n",
      "\n",
      "a[-1]:\n",
      " array([[7, 8, 9]], dtype=uint8)\n",
      "\n",
      "a[-1:-3:-1]:\n",
      " array([[7, 8, 9],\n",
      "\t[4, 5, 6]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)\n",
    "print('a:\\n', a)\n",
    "\n",
    "# the first row\n",
    "print('\\na[0]:\\n', a[0])\n",
    "\n",
    "# the first two elements of the first row\n",
    "print('\\na[0,:2]:\\n', a[0,:2])\n",
    "\n",
    "# the zeroth element in each row (also known as the zeroth column)\n",
    "print('\\na[:,0]:\\n', a[:,0])\n",
    "\n",
    "# the last row\n",
    "print('\\na[-1]:\\n', a[-1])\n",
    "\n",
    "# the last two rows backwards\n",
    "print('\\na[-1:-3:-1]:\\n', a[-1:-3:-1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Assignment to slices can be done for the whole slice, per row, and per column. A couple of examples should make these statements clearer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T17:40:24.031254Z",
     "start_time": "2020-10-12T17:40:24.011816Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([[0, 0, 0],\n",
      "\t[0, 0, 0],\n",
      "\t[0, 0, 0]], dtype=uint8)\n",
      "\n",
      "a[0] = 1\n",
      " array([[1, 1, 1],\n",
      "\t[0, 0, 0],\n",
      "\t[0, 0, 0]], dtype=uint8)\n",
      "\n",
      "a[:,0]:\n",
      " array([[0, 0, 3],\n",
      "\t[0, 0, 3],\n",
      "\t[0, 0, 3]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.zeros((3, 3), dtype=np.uint8)\n",
    "print('a:\\n', a)\n",
    "\n",
    "# assigning to the whole row\n",
    "a[0] = 1\n",
    "print('\\na[0] = 1\\n', a)\n",
    "\n",
    "a = np.zeros((3, 3), dtype=np.uint8)\n",
    "\n",
    "# assigning to a column\n",
    "a[:,2] = 3.0\n",
    "print('\\na[:,0]:\\n', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, you should notice that we re-set the array `a` after the first assignment. Do you care to see what happens, if we do not do that? Well, here are the results:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-12T17:44:09.180623Z",
     "start_time": "2020-10-12T17:44:09.161578Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:  array([[1, 1, 3],\n",
      "\t[0, 0, 3],\n",
      "\t[0, 0, 3]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.zeros((3, 3), dtype=np.uint8)\n",
    "b = a[:,:]\n",
    "# assign 1 to the first row\n",
    "b[0] = 1\n",
    "\n",
    "# assigning to the last column\n",
    "b[:,2] = 3\n",
    "print('a: ', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that both assignments involved `b`, and not `a`, yet, when we print out `a`, its entries are updated. This proves our earlier statement about the behaviour of *views*: in the statement `b = a[:,:]` we simply created a *view* of `a`, and not a *deep* copy of it, meaning that whenever we modify `b`, we actually modify `a`, because the underlying data container of `a` and `b` are shared between the two object. Having a single data container for two seemingly different objects provides an extremely powerful way of manipulating sub-sets of numerical data."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you want to work on a *copy* of your data, you can use the `.copy` method of the `ndarray`. The following snippet should drive the point home:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 90,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-17T13:06:20.223171Z",
     "start_time": "2020-10-17T13:06:20.206422Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class: ndarray\n",
      "shape: (3, 3)\n",
      "strides: (3, 1)\n",
      "itemsize: 1\n",
      "data pointer: 0x7ff737ea3220\n",
      "type: uint8\n",
      "\n",
      "class: ndarray\n",
      "shape: (3, 3)\n",
      "strides: (3, 1)\n",
      "itemsize: 1\n",
      "data pointer: 0x7ff737ea3340\n",
      "type: uint8\n",
      "\n",
      "a:  array([[0, 0, 0],\n",
      "\t[0, 0, 0],\n",
      "\t[0, 0, 0]], dtype=uint8)\n",
      "====================\n",
      "b:  array([[1, 1, 1],\n",
      "\t[0, 0, 0],\n",
      "\t[0, 0, 0]], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.zeros((3, 3), dtype=np.uint8)\n",
    "b = a.copy()\n",
    "\n",
    "# get the address of the underlying data pointer\n",
    "\n",
    "np.ndinfo(a)\n",
    "print()\n",
    "np.ndinfo(b)\n",
    "\n",
    "# assign 1 to the first row of b, and do not touch a\n",
    "b[0] = 1\n",
    "\n",
    "print()\n",
    "print('a: ', a)\n",
    "print('='*20)\n",
    "print('b: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `.copy` method can also be applied to views: below, `a[0]` is a *view* of `a`, out of which we create a *deep copy* called `b`. This is a row vector now. We can then do whatever we want to with `b`, and that leaves `a` unchanged."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-10-17T13:00:06.217232Z",
     "start_time": "2020-10-17T13:00:06.207417Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "b:  array([0, 0, 0], dtype=uint8)\n",
      "====================\n",
      "a:  array([[0, 0, 0],\n",
      "\t[0, 0, 0],\n",
      "\t[0, 0, 0]], dtype=uint8)\n",
      "====================\n",
      "b:  array([1, 0, 0], dtype=uint8)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.zeros((3, 3), dtype=np.uint8)\n",
    "b = a[0].copy()\n",
    "print('b: ', b)\n",
    "print('='*20)\n",
    "# assign 1 to the first entry of b, and do not touch a\n",
    "b[0] = 1\n",
    "print('a: ', a)\n",
    "print('='*20)\n",
    "print('b: ', b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The fact that the underlying data of a view is the same as that of the original array has another important consequence, namely, that the creation of a view is cheap. Both in terms of RAM, and execution time. A view is really nothing but a short header with a data array that already exists, and is filled up. Hence, creating the view requires only the creation of its header. This operation is fast, and uses virtually no RAM."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "382.797px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
